/*
 * Copyright by LunaSec (owned by Refinery Labs, Inc)
 *
 * Licensed under the Business Source License v1.1
 * (the "License"); you may not use this file except in compliance with the
 * License. You may obtain a copy of the License at
 *
 * https://github.com/lunasec-io/lunasec/blob/master/licenses/BSL-LunaTrace.txt
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

import { SeverityNamesOsv, severityOrderOsv } from '@lunatrace/lunatrace-common';
import cvss from 'cvss';
import minimatch from 'minimatch';
import { parseCvss3Vector } from 'vuln-vects';

import { Analysis_Finding_Source_Enum, Analysis_Finding_Type_Enum } from '../../../hasura-api/generated';
import {
  Adjustment,
  FolderSettings,
  ManifestNode,
  PackageVulnerability,
  TriagedPackageVulnerability,
  VulnerableRelease,
} from '../types';
import { VulnerabilityProcessor } from '../vulnerability-processor';

import { modifyVectorString, parseAdjustment } from './vector-strings';

// takes in a vulnerable release and the list of folder settings configured on the project, determines which to apply, if any, and adjusts the score
// this happens as a final pass over the vulnerable releases, because at that point the tree is fully built and the chains are populated
// This modifies the vulnerable releases in place rather than returning new objects
export function adjustRelease(
  vulnerableRelease: VulnerableRelease,
  folderSettings: FolderSettings,
  minimumSeverity: SeverityNamesOsv | undefined
): void {
  // get the root paths
  const resultLocations = vulnerableRelease.chains.flatMap((chain) => chain[1].locations);

  const firstPartyPaths = resultLocations.map((location) => location.path);
  const hasPaths = firstPartyPaths.length !== 0;

  // find a setting that matches all of the paths, starting with the highest precedence settings and ending with the root setting as thats the order it comes from hasura
  const matchingFolderSetting = folderSettings.find((setting) => {
    if (setting.root === true) {
      return true;
    }
    if (!hasPaths) {
      return false;
    }
    return firstPartyPaths.every((path) => minimatch(path, setting.path_glob));
  });

  if (!matchingFolderSetting) {
    // nothing to do, bail
    return;
  }

  const adjustmentNames = matchingFolderSetting.folder_environmental_adjustments.map(
    (a) => a.cvss_environmental_adjustment.name
  );
  const pathMatched = matchingFolderSetting.root ? 'Project Root' : matchingFolderSetting.path_glob;

  if (matchingFolderSetting.ignore) {
    ignoreVulnerableRelease(vulnerableRelease, pathMatched);
    return;
  }
  if (matchingFolderSetting.folder_environmental_adjustments.length === 0) {
    // matched a setting with no adjustments, typically the un-configured root setting. Bail.
    return;
  }

  // go through each vulnerability and perform the adjustment.
  const vulnerabilities = vulnerableRelease.affected_by;
  vulnerabilities.forEach((vuln) => {
    adjustOneVulnerability(vuln, matchingFolderSetting, pathMatched, adjustmentNames);
  });
  // Add some helpful information to the overall release about what settings have been applied
  mergeVulnAdjustmentsOntoRelease(vulnerableRelease, pathMatched, adjustmentNames);
  recomputeBeneathMinimumSeverity(vulnerableRelease, minimumSeverity);
}

// Ignoring is a special case handled differently from adjustment
function ignoreVulnerableRelease(vulnRelease: VulnerableRelease, pathMatched: string): void {
  vulnRelease.affected_by.forEach((vuln) => {
    vuln.ignored = true;
    vuln.ignored_vulnerability = {
      note: 'Risk adjustment match: ' + pathMatched,
    };
  });

  // vulnRelease.
}

// Do the same logic the beneath minimum severity check did once more since the severity may ahve been adjusted
function recomputeBeneathMinimumSeverity(
  vulnRelease: VulnerableRelease,
  minimumSeverity: SeverityNamesOsv | undefined
) {
  if (!minimumSeverity) {
    return;
  }
  vulnRelease.affected_by.forEach((vuln) => {
    vuln.beneath_minimum_severity = VulnerabilityProcessor.severityNameBelowLimit(
      vuln.vulnerability.severity_name,
      minimumSeverity
    );
  });
  vulnRelease.beneath_minimum_severity = VulnerabilityProcessor.severityNameBelowLimit(
    vulnRelease.severity,
    minimumSeverity
  );
}

function adjustOneVulnerability(
  vuln: TriagedPackageVulnerability,
  setting: FolderSettings[number],
  pathMatched: string,
  adjustmentNames: string[]
): void {
  // While our database was designed to support multiple severities per vulnerability, in reality we only seem to have one per vuln
  const severityObject = vuln.vulnerability.severities[0];
  // handle only cvss v3, that's all we have in the DB at the time of writing
  if (!severityObject || severityObject.type !== 'CVSS_V3') {
    return;
  }
  const vectorString = severityObject.score;
  // parse each adjustment into an object of vector subcomponents like {CA:N,R:M,...}
  const adjustmentObjects = setting.folder_environmental_adjustments.map((adjustment) =>
    parseAdjustment(adjustment.cvss_environmental_adjustment)
  );
  // merge all of the adjustments onto the original vector string from the vulnerability
  const modifiedVector = adjustmentObjects.reduce<string>((vectorString, adjustmentObject) => {
    return modifyVectorString(vectorString, adjustmentObject);
  }, vectorString);

  // use this vuln-vects library to turn the cvss vector into a numerical value. the library is very poorly written but does seem to work
  const environmentalCvss = parseCvss3Vector(modifiedVector);
  const adjustedScore = environmentalCvss.overallScore;
  // Because vuln-vects doesnt give us a rating by name, use this other cvss library (which is broken for processing vectors btw) to convert the numeric score to a rating name
  const adjustedRating = cvss.getRating(adjustedScore);

  if (adjustedScore === vuln.vulnerability.cvss_score) {
    // adjustment did nothing, bail
    return;
  }

  if (!vuln.vulnerability.severity_name || !vuln.vulnerability.cvss_score) {
    // this is just to makes types pass, it won't really ever happen
    return;
  }
  // preserve the old severity so we can show it in little letters underneath and show what happened
  vuln.adjustment = {
    adjusted_from_cvss_score: vuln.vulnerability.cvss_score,
    adjusted_from_severity_name: vuln.vulnerability.severity_name,
    path_matched: pathMatched,
    adjustments_applied: adjustmentNames,
  };
  //overwrite the main score so that all existing sorting, filtering, and alerting continue to work without special logic for reading adjusted values
  vuln.vulnerability.cvss_score = adjustedScore;
  vuln.vulnerability.severity_name = adjustedRating;
  // reprocess whether the vuln is beneath minimum severity or not
  return;
}

function getSeverityRankAsInteger(vuln: TriagedPackageVulnerability) {
  return severityOrderOsv.indexOf(vuln.vulnerability.severity_name || 'Unknown');
}

function mergeVulnAdjustmentsOntoRelease(
  vulnRelease: VulnerableRelease,
  pathMatched: string,
  adjustmentsApplied: string[]
): void {
  const sortedVulns = vulnRelease.affected_by.sort((vuln1, vuln2) => {
    return getSeverityRankAsInteger(vuln2) - getSeverityRankAsInteger(vuln1);
  });
  const mostSevereVuln = sortedVulns[0];

  if (mostSevereVuln.vulnerability.cvss_score === vulnRelease.cvss) {
    // Adjustments did nothing to the overall score, bail
    return;
  }

  vulnRelease.adjustment = {
    adjusted_from_cvss_score: vulnRelease.cvss,
    adjusted_from_severity_name: vulnRelease.severity,
    path_matched: pathMatched,
    adjustments_applied: adjustmentsApplied,
  };

  vulnRelease.cvss = mostSevereVuln.vulnerability.cvss_score || null;
  vulnRelease.severity = mostSevereVuln.vulnerability.severity_name || 'Unknown';

  return;
}
